
export const BOX_ANCHOR_POS = {
  TOP_LEFT: 1,
  TOP_RIGHT: 2,
  BOTTOM_RIGHT: 3,
  BOTTOM_LEFT: 4
}

export const fitSquarePoint = (movingPoint: { x: number, y: number }, fixedPoint: { x: number, y: number }) => {
  const mp = movingPoint
  const fp = fixedPoint
  const xlen = Math.abs(mp.x - fp.x)
  const ylen = Math.abs(mp.y - fp.y)
  const len = Math.min(xlen, ylen)

  return {
    x: fp.x + Math.sign(mp.x - fp.x) * len,
    y: fp.y + Math.sign(mp.y - fp.y) * len
  }
}

export const calcRectAndAnchor = (movingPoint: { x: number, y: number }, fixedPoint: { x: number, y: number }) => {
  const mp = movingPoint
  const fp = fixedPoint
  let pos = null
  let tlp = null

  if (mp.x <= fp.x && mp.y <= fp.y) {
    pos = BOX_ANCHOR_POS.TOP_LEFT
    tlp = mp
  } else if (mp.x > fp.x && mp.y > fp.y) {
    pos = BOX_ANCHOR_POS.BOTTOM_RIGHT
    tlp = fp
  } else if (mp.x > fp.x) {
    pos = BOX_ANCHOR_POS.TOP_RIGHT
    tlp = { x: fp.x, y: mp.y }
  } else if (mp.y > fp.y) {
    pos = BOX_ANCHOR_POS.BOTTOM_LEFT
    tlp = { x: mp.x, y: fp.y }
  }

  return {
    rect: {
      x: tlp!.x,
      y: tlp!.y,
      width: Math.abs(mp.x - fp.x),
      height: Math.abs(mp.y - fp.y)
    },
    anchorPos: pos
  }
}

export const pointAtPos = (rect: { x: number, y: number, width: number, height: number }, pos?: number) => {
  switch (pos) {
    case BOX_ANCHOR_POS.TOP_LEFT:
      return {
        x: rect.x,
        y: rect.y
      }
    case BOX_ANCHOR_POS.TOP_RIGHT:
      return {
        x: rect.x + rect.width,
        y: rect.y
      }
    case BOX_ANCHOR_POS.BOTTOM_RIGHT:
      return {
        x: rect.x + rect.width,
        y: rect.y + rect.height
      }
    case BOX_ANCHOR_POS.BOTTOM_LEFT:
      return {
        x: rect.x,
        y: rect.y + rect.height
      }
  }
  return {
    x: 0,
    y: 0
  }
}

export const diagonalPos = (pos: number) => {
  switch (pos) {
    case BOX_ANCHOR_POS.TOP_LEFT:
      return BOX_ANCHOR_POS.BOTTOM_RIGHT

    case BOX_ANCHOR_POS.TOP_RIGHT:
      return BOX_ANCHOR_POS.BOTTOM_LEFT

    case BOX_ANCHOR_POS.BOTTOM_RIGHT:
      return BOX_ANCHOR_POS.TOP_LEFT

    case BOX_ANCHOR_POS.BOTTOM_LEFT:
      return BOX_ANCHOR_POS.TOP_RIGHT
  }

}

export const diagonalPoint = (rect: { x: number, y: number, width: number, height: number }, anchorPos: number) => {
  return pointAtPos(rect, diagonalPos(anchorPos))
}

export const genGetAnchorRects = (ANCHOR_POS: any, pointAtPos: any) => ({ rect, size = 5 }: { rect: { x: number, y: number, width: number, height: number }, size: number }) => {
  const values = (obj: any) => Object.keys(obj).map(key => obj[key])
  const createRect = (point: { x: number, y: number }, size: number) => ({
    x: point.x - size,
    y: point.y - size,
    width: size * 2,
    height: size * 2
  })

  return values(ANCHOR_POS).map(pos => {
    return {
      anchorPos: pos,
      rect: createRect(pointAtPos(rect, pos), size)
    }
  })
}

export const getAnchorRects = genGetAnchorRects(BOX_ANCHOR_POS, pointAtPos)

export class Box {
  // Note: possible settings
  static settings = []
  static category = 'rect'
  static defaultAnchorPos = BOX_ANCHOR_POS.BOTTOM_RIGHT

  transform: any
  onStateChange: any
  normalizeRect: any

  state = {
    id: null,
    type: 'box',
    data: null,
    style: {},
    rect: {
      x: 0,
      y: 0,
      width: 0,
      height: 0
    }
  }

  local: any = {}

  constructor(options: any) {
    const opts = Object.assign({
      firstSilence: true,
      transform: (x: any) => x,
      onStateChange: () => { }
    }, options)

    this.transform = opts.transform
    this.onStateChange = opts.onStateChange
    this.normalizeRect = opts.normalizeRect || ((x:any) => x)

    this.__setState({
      id: opts.id,
      data: opts.data,
      type: this.getType(),
      style: this.getDefaultStyle(),
      category: this.getCategory(),
      rect: {
        x: opts.x,
        y: opts.y,
        width: opts.width || 0,
        height: opts.height || 0
      }
    }, { silent: opts.firstSilence })
  }

  getType() {
    return 'box'
  }

  getCategory() {
    return Box.category
  }

  getDefaultAnchorPos() {
    return BOX_ANCHOR_POS.BOTTOM_RIGHT
  }

  getDefaultStyle() {
    return {}
  }

  getId() {
    return this.state.id
  }

  getState() {
    return this.transform(this.state)
  }

  processIncomingStyle(style: any) {
    return style
  }

  setStyle(obj: any) {
    this.__setState({
      style: {
        ...this.state.style,
        ...this.processIncomingStyle(obj)
      }
    })
  }

  setData(data: any) {
    this.__setState({ data })
  }

  moveAnchorStart({ anchorPos }: { anchorPos: number }) {
    this.__setLocal({
      oldPoint: pointAtPos(this.state.rect, anchorPos),
      oldAnchorPos: anchorPos,
      anchorPos: anchorPos
    })
  }

  moveAnchor({ x, y }: { x: number, y: number }, { fit }: { fit?: boolean } = {}) {
    const old = this.state.rect
    const pos = this.local.anchorPos
    const fixed = diagonalPoint(old, pos)
    const moving = !fit ? { x, y } : fitSquarePoint({ x, y }, fixed)
    const res = calcRectAndAnchor(moving, fixed)

    this.__setLocal({ anchorPos: res.anchorPos })
    this.__setState({ rect: this.normalizeRect(res.rect, 'moveAnchor') })
  }

  moveAnchorEnd() {
    this.__setLocal({
      oldPoint: null,
      oldAnchorPos: null,
      anchorPos: null
    })
  }

  moveBoxStart() {
    this.__setLocal({
      oldRect: { ...this.state.rect }
    })
  }

  moveBox({ dx, dy }: { dx: number, dy: number }) {
    const old = this.local.oldRect
    const upd = {
      ...old,
      x: old.x + dx,
      y: old.y + dy
    }

    this.__setState({ rect: this.normalizeRect(upd, 'moveBox') })
  }

  moveBoxEnd() {
    this.__setLocal({
      oldRect: null
    })
  }

  __setState(obj: any, opts: any = {}) {
    const last = this.getState()

    this.state = {
      ...this.state,
      ...obj
    }

    if (opts.silent) return

    const fn = () => this.onStateChange(this.getState(), last)
    const invoke = opts.nextTick ? (fn: any) => setTimeout(fn, 0) : (fn: any) => fn()

    invoke(fn)
  }

  __setLocal(obj: any) {
    this.local = {
      ...this.local,
      ...obj
    }
  }
}
